﻿Command = function () {

};

Command = function (vm, commandInfo) {
    this.vm = vm;
    this.commandInfo = commandInfo;
    this.getCommandFunction();
};

Command.prototype.Execute = function () {
    if (this.commandFunction)
        this.commandFunction(this);
};

Command.prototype.getCommandFunction = function () {
    this.commandFunction = this["_" + this.commandInfo.Name];

    if (!this.commandFunction)
        throw "CommandNotFoundException: " + this.commandInfo.Name;
};

Command.prototype.assertStackMinimum = function (size) {
    if (this.vm.State.Stack.GetSize() < size)
        throw "StackSizeException";
};

// --------------------------------------------------------------------------------------------
// -- shared implementations ------------------------------------------------------------------
// --------------------------------------------------------------------------------------------

Command.prototype.getBinaryNumericOperationResult = function (type1, type2) {
    if (type1 == "i4") {
        if (type2 == "i4")
            return "i4";
        if (type2 == "nativeint")
            return "nativeint";
    }
    else if (type1 == "i8") {
        if (type2 == "i8")
            return "i8";
    }
    else if (type1 == "nativeint") {
        if (type2 == "i4" || type2 == "nativeint")
            return "nativeint";
    }
    else if (type1 == "float" && type2 == "float")
        return "float";

    throw "InvalidBinaryOperation";
};

Command.prototype.getIntegerOperationResult = function (type1, type2) {
    if (type1 == "i4") {
        if (type2 == "i4")
            return "i4";
        if (type2 == "nativeint")
            return "nativeint";
    }
    else if (type1 == "i8") {
        if (type2 == "i8")
            return "i8";
    }
    else if (type1 == "nativeint") {
        if (type2 == "i4" || type2 == "nativeint")
            return "nativeint";
    }
    
    throw "InvalidIntegerOperation";
};

Command.prototype.getShiftOperationResult = function (type1, type2) {
    if (type1 == "i4") {
        if (type2 == "i4" || type2 == "nativeint")
            return "i4";
    }
    else if (type1 == "i8") {
        if (type2 == "i4" || type2 == "nativeint")
            return "i8";
    }
    else if (type1 == "nativeint") {
        if (type2 == "i4" || type2 == "nativeint")
            return "nativeint";
    }

    throw "InvalidShiftOperation";
};

Command.prototype.isValidBinaryComparison = function (type1, type2) {
    if (type1 == "i4" || type1 == "nativeint")
        return type2 == "i4" || type2 == "nativeint";

    if (type1 == "i8")
        return type2 == "i8";

    if (type1 == "float")
        return type2 == "float";

    return false;
};

Command.prototype.bitwiseOperation = function (operation) {
    this.assertStackMinimum(2);

    var value2 = this.vm.State.Stack.Pop();
    var value1 = this.vm.State.Stack.Pop();

    var result = new StackEntry();
    result.Type = this.getIntegerOperationResult(value1.Type, value2.Type);
    result.Value = operation(value1.Value, value2.Value);

    this.vm.State.Stack.Push(result);
};

Command.prototype.binaryNumpericOperation = function (operation) {
    this.assertStackMinimum(2);

    var value2 = this.vm.State.Stack.Pop();
    var value1 = this.vm.State.Stack.Pop();

    var result = new StackEntry();
    result.Type = this.getBinaryNumericOperationResult(value1.Type, value2.Type);
    result.Value = operation(value1.Value, value2.Value);

    this.vm.State.Stack.Push(result);
};

Command.prototype.binaryComparison = function (operation) {
    this.assertStackMinimum(2);
    var value2 = this.vm.State.Stack.Pop();
    var value1 = this.vm.State.Stack.Pop();

    if (!this.isValidBinaryComparison(value1.Type, value2.Type))
        throw "InvalidComparisonException";

    var result = new StackEntry();
    result.Type = "int4";
    result.Value = operation(value1.Value, value2.Value) ? 1 : 0;

    this.vm.State.Stack.Push(result);
};

Command.prototype.binaryComparisonBranch = function (operation) {
    this.assertStackMinimum(2);

    var value2 = this.vm.State.Stack.Pop();
    var value1 = this.vm.State.Stack.Pop();

    if (!this.isValidBinaryComparison(value1.Type, value2.Type))
        throw "InvalidComparisonException";

    if (operation(value1.Value, value2.Value)) {
        var offset = this.getBranchOffset();
        this.vm.Branch(offset);
    }
};

Command.prototype.getBranchOffset = function () {
    if (this.commandInfo.Arguments.length != 1)
        throw "InvalidCommandException";

    var offset;
    try {
        offset = parseInt(this.commandInfo.Arguments[0]);
    }
    catch (ex) {
        throw "ArgumentException: " + ex.toString();
    }

    return offset;
};


// --------------------------------------------------------------------------------------------
// -- base instructions -----------------------------------------------------------------------
// --------------------------------------------------------------------------------------------

Command.prototype._add = function () {
    this.binaryNumpericOperation(function (value1, value2) {
        return value1 + value2;
    });
};

Command.prototype._and = function () {
    this.bitwiseOperation(function (value1, value2) {
        return value1 & value2;
    });
};

Command.prototype._arglist = function () {

};

Command.prototype._beq = function () {
    this.binaryComparisonBranch(function (value1, value2) {
        return value1 == value2;
    });
};

Command.prototype._bge = function () {
    this.binaryComparisonBranch(function (value1, value2) {
        return value1 >= value2;
    });
};

Command.prototype._bgt = function () {
    this.binaryComparisonBranch(function (value1, value2) {
        return value1 > value2;
    });
};

Command.prototype._ble = function () {
    this.binaryComparisonBranch(function (value1, value2) {
        return value1 <= value2;
    });
};

Command.prototype._blt = function () {
    this.binaryComparisonBranch(function (value1, value2) {
        return value1 < value2;
    });
};

Command.prototype._bne = function () {
    this.binaryComparisonBranch(function (value1, value2) {
        return value1 != value2;
    });
};

Command.prototype._br = function () {
    var offset = this.getBranchOffset();
    this.vm.Branch(offset);
};

Command.prototype._break = function () {

};

Command.prototype._brfalse = function () {
    this.assertStackMinimum(1);
    var top = this.vm.State.Stack.Pop();

    if (!top.Value) {
        var offset = this.getBranchOffset();
        this.vm.Branch(offset);
    }
};

Command.prototype._brnull = function () {
    this._brfalse();
};

Command.prototype._brtrue = function () {
    this.assertStackMinimum(1);
    var top = this.vm.State.Stack.Pop();

    if (top.Value) {
        var offset = this.getBranchOffset();
        this.vm.Branch(offset);
    }
};

Command.prototype._brzero = function () {
    this._brfalse();
};

Command.prototype._call = function () {
    var methodDescription = this.commandInfo.Arguments[0];
    if (methodDescription.indexOf("write") >= 0) {
        this.assertStackMinimum(1);
        var output = this.vm.State.Stack.Pop();
        if (methodDescription.indexOf("line") >= 0)
            this.vm.Console.WriteLine(output.Value);
        else
            this.vm.Console.Write(output.Value);
    }
    else {
        // halt the vm to wait for IO to return
        this.vm.Pause();
        this.vm.Console.ReadLine(
            function (vm) {
                return function (input) {
                    var newSlot = new StackEntry();
                    newSlot.Type = "string";
                    newSlot.Value = input;
                    vm.State.Stack.Push(newSlot);
                    vm.Resume();
                };
            } (this.vm)
        );
    }
};

Command.prototype._calli = function () {

};

Command.prototype._ceq = function () {
    this.binaryComparison(function (value1, value2) {
        return value1 == value2;
    });
};

Command.prototype._cgt = function () {
    this.binaryComparison(function (value1, value2) {
        return value1 > value2;
    });
};

Command.prototype._ckfinite = function () {

};

Command.prototype._clt = function () {
    this.binaryComparison(function (value1, value2) {
        return value1 < value2;
    });
};

Command.prototype._conv = function () {

};

Command.prototype._cpblk = function () {

};

Command.prototype._div = function () {

};

Command.prototype._dup = function () {
    this.assertStackMinimum(1);
    var top = this.vm.State.Stack.Pop();
    this.vm.State.Stack.Push(top);
    this.vm.State.Stack.Push(top.Copy());
};

Command.prototype._endfilter = function () {

};

Command.prototype._endfinally = function () {

};

Command.prototype._initblk = function () {

};

Command.prototype._jmp = function () {

};

Command.prototype._ldarg = function () {
    var index = -1;

    if (this.commandInfo.Modifiers.length == 0 || (this.commandInfo.Modifiers.length == 1 && this.commandInfo.Modifiers[0] == "s")) {
        if (this.commandInfo.Arguments.length != 1)
            throw "ArgumentException";

        try {
            index = parseInt(this.commandInfo.Arguments[0]);
        }
        catch (ex) {
            throw "ArgumentException: " + ex.toString();
        }
    }
    else if (this.commandInfo.Modifiers.length == 1) {
        try {
            index = parseInt(this.commandInfo.Modifiers[0]);
            if (index < 0 || index > 3)
                throw "CommandException";
        }
        catch (ex2) {
            throw ex2.toString();
        }
    }

    if (index < 0 || index > this.vm.State.Arguments.length)
        throw "IndexOutOfRangeException";

    this.vm.State.Stack.Push(this.vm.State.Arguments[index].Copy());
};

Command.prototype._ldarga = function () {

};

Command.prototype._ldc = function () {
    if (this.commandInfo.Modifiers.length == 0)
        throw "CommandException";

    var result = new StackEntry();
    var type = this.commandInfo.Modifiers[0];

    if (type == "i8") {
        result.Type = "i8";
        result.Value = parseInt(this.commandInfo.Arguments[0]);
    }
    else if (type == "r4" || type == "r8") {
        if (this.commandInfo.Modifiers.length != 1 || this.commandInfo.Arguments.length != 1)
            throw "CommandException";

        result.Type = "float";

        try {
            result.Value = parseFloat(this.commandInfo.Arguments[0]);
        }
        catch (ex) {
            throw "ArgumentException: " + ex.toString();
        }
    }
    else if (type == "i4") {
        result.Type = type;

        if (this.commandInfo.Modifiers.length > 2)
            throw "CommandException";

        try {
            if (this.commandInfo.Modifiers.length == 2) {
                var modifier = this.commandInfo.Modifiers[1];

                if (modifier == "m1" || modifier == "M1")
                    result.Value = -1;
                else if (modifier == "s")
                    result.Value = parseInt(this.commandInfo.Arguments[0]);
                else {
                    var intValue = parseInt(modifier);
                    if (intValue < 0 || intValue > 8)
                        throw "CommandException";
                    else
                        result.Value = intValue;
                }
            }
            else
                result.Value = parseInt(this.commandInfo.Arguments[0]);

        }
        catch (ex2) {
            throw "CommandException: " + ex2.toString();
        }
    }
    else {
        throw "CommandException: Illegal modifier";
    }


    this.vm.State.Stack.Push(result);
};

Command.prototype._ldftn = function () {

};

Command.prototype._ldind = function () {

};

Command.prototype._ldloc = function () {
    var index;

    if (this.commandInfo.Modifiers.length == 0 || this.commandInfo.Modifiers[0] == "s") {

        // must have at least one argument
        if (this.commandInfo.Arguments.length != 1)
            throw "InvalidCommandException";

        var indexString = this.commandInfo.Arguments[0];

        try {
            index = parseInt(indexString);
        }
        catch (ex) {
            throw "InvalidCommandException: " + ex.toString();
        }

        if (index < 0)
            throw "IndexOutOfRangeException: Locals(" + index + ")";
    }
    else {
        var indexModifier = this.commandInfo.Modifiers[0];

        try {
            index = parseInt(indexModifier);
        }
        catch (ex) {
            throw "InvalidCommandException: " + ex.toString();
        }

        if (index < 0 || index > 3)
            throw "IndexOutOfRangeException: Locals(" + index + ")";
    }

    var memoryValue = this.vm.State.Locals.Get(index);
    if (!memoryValue)
        throw "IndexOutOfRangeException";

    this.vm.State.Stack.Push(memoryValue.Copy());
};

Command.prototype._ldloca = function () {

};

Command.prototype._ldnull = function () {
    var newValue = new StackEntry();
    newValue.Type = "Object";
    newValue.Value = null;
    this.vm.State.Stack.Push(newValue);
};

Command.prototype._leave = function () {

};

Command.prototype._localloc = function () {

};

Command.prototype._mul = function () {
    this.binaryNumpericOperation(function (value1, value2) {
        return value1 * value2;
    });
};

Command.prototype._neg = function () {
    this.assertStackMinimum(1);

    var value1 = this.vm.State.Stack.Pop();
    if (value1.Type != "i4" && value1.Type != "nativeint" && value1.Type != "nativeint" && value1.Type != "float")
        throw "UnaryOperationException";

    if (value1.Value == "NaN")
        value1.Value = "NaN";
    if (value1.Type == "float")
        value1.Value = 0 - value1.Value;
    else
        value1.Value = ~value1.Value + 1;

    this.vm.State.Stack.Push(value1);
};

Command.prototype._nop = function () {
    // do nothing
};

Command.prototype._not = function () {
    this.assertStackMinimum(1);

    var value1 = this.vm.State.Stack.Pop();
    if (value1.Type != "i4" && value1.Type != "nativeint" && value1.Type != "nativeint")
        throw "IntegerOperationException";

    value1.Value = ~value1.Value;
    this.vm.State.Stack.Push(value1);
};

Command.prototype._or = function () {
    this.bitwiseOperation(function (value1, value2) {
        return value1 | value2;
    });
};

Command.prototype._pop = function () {
    this.assertStackMinimum(1);
    this.vm.State.Stack.Pop();
};

Command.prototype._rem = function () {

};

Command.prototype._ret = function () {
    this.vm.Return();
};

Command.prototype._shl = function () {
    this.assertStackMinimum(2);

    var shiftAmount = this.vm.State.Stack.Pop();
    var shiftValue = this.vm.State.Stack.Pop();

    var result = new StackEntry();
    result.Type = this.getShiftOperationResult(shiftValue.Type, shiftAmount.Type);
    result.Value = shiftValue.Value << shiftAmount.Value;

    this.vm.State.Stack.Push(result);
};

Command.prototype._shr = function () {
    this.assertStackMinimum(2);

    var shiftAmount = this.vm.State.Stack.Pop();
    var shiftValue = this.vm.State.Stack.Pop();

    var result = new StackEntry();
    result.Type = this.getShiftOperationResult(shiftValue.Type, shiftAmount.Type);

    if (this.commandInfo.Modifiers.length > 0) {
        if (this.commandInfo.Modifiers[0] == "un")
            result.Value = shiftValue.Value >>> shiftAmount.Value;
        else
            throw "InvalidCommandException";
    }
    else
        result.Value = shiftValue.Value >> shiftAmount.Value;
    
    this.vm.State.Stack.Push(result);
};

Command.prototype._starg = function () {

};

Command.prototype._stind = function () {

};

Command.prototype._stloc = function () {
    this.assertStackMinimum(1);
    var index;

    if (this.commandInfo.Modifiers.length == 0 || this.commandInfo.Modifiers[0] == "s") {

        // must have at least one argument
        if (this.commandInfo.Arguments.length != 1)
            throw "InvalidCommandException";

        var indexString = this.commandInfo.Arguments[0];

        try {
            index = parseInt(indexString);
        }
        catch (ex) {
            throw "InvalidCommandException: " + ex.toString();
        }

        if (index < 0)
            throw "IndexOutOfRangeException: Locals(" + index + ")";
    }
    else {
        var indexModifier = this.commandInfo.Modifiers[0];

        try {
            index = parseInt(indexModifier);
        }
        catch (ex) {
            throw "InvalidCommandException: " + ex.toString();
        }

        if (index < 0 || index > 3)
            throw "IndexOutOfRangeException: Locals(" + index + ")";
    }

    var value1 = this.vm.State.Stack.Pop();
    this.vm.State.Locals.Set(index, value1);
};

Command.prototype._sub = function () {
    this.binaryNumpericOperation(function (value1, value2) {
        return value1 - value2;
    });
};

Command.prototype._switch = function () {

};

Command.prototype._xor = function () {
    this.bitwiseOperation(function (value1, value2) {
        return value1 ^ value2;
    });
};


// --------------------------------------------------------------------------------------------
// -- object model instructions ---------------------------------------------------------------
// --------------------------------------------------------------------------------------------

Command.prototype._box = function () {

};

Command.prototype._callvirt = function () {

};

Command.prototype._castclass = function () {

};

Command.prototype._cpobj = function () {

};

Command.prototype._initobj = function () {

};

Command.prototype._isinst = function () {

};

Command.prototype._ldelem = function () {

};

Command.prototype._ldelema = function () {

};

Command.prototype._ldfld = function () {

};

Command.prototype._ldflda = function () {

};

Command.prototype._ldlen = function () {

};

Command.prototype._ldobj = function () {

};

Command.prototype._ldsfld = function () {

};

Command.prototype._ldsflda = function () {

};

Command.prototype._ldstr = function () {
    if (this.commandInfo.Arguments.length == 0)
        throw "ArgumentException";

    var stringLiteral = this.commandInfo.Arguments.join(" ");

    if (stringLiteral.length < 2 || stringLiteral.substr(0, 1) != '"' || stringLiteral.substr(stringLiteral.length - 1, 1) != '"')
        throw "ArgumentException";

    var stringValue = stringLiteral.substr(1, stringLiteral.length - 2);
    var newEntry = new StackEntry();
    newEntry.Type = "string";
    newEntry.Value = stringValue;
    this.vm.State.Stack.Push(newEntry);
};

Command.prototype._ldtoken = function () {

};

Command.prototype._ldvirtftn = function () {

};

Command.prototype._mdrefany = function () {

};

Command.prototype._newarr = function () {

};

Command.prototype._newobj = function () {

};

Command.prototype._refanytype = function () {

};

Command.prototype._refanyval = function () {

};

Command.prototype._rethrow = function () {

};

Command.prototype._sizeof = function () {

};

Command.prototype._stelem = function () {

};

Command.prototype._stfld = function () {

};

Command.prototype._stobj = function () {

};

Command.prototype._stsfld = function () {

};

Command.prototype._throw = function () {

};

Command.prototype._unbox = function () {

};
